#  /**
#  * Copyright (c) 2022 Beijing Jiaotong University
#  * PhotLab is licensed under [Open Source License].
#  * You can use this software according to the terms and conditions of the [Open Source License].
#  * You may obtain a copy of [Open Source License] at: [https://open.source.license/]
#  *
#  * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
#  * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
#  * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#  *
#  * See the [Open Source License] for more details.
#  */
#  /**
#  * Author: Zheng Wang
#  * Created: Jun. 27, 2022
#  * Supported by: National Key Research and Development Program of China
#  */
import logging

import numpy as np

from phot_engine.computing_graph import Edge
from phot_engine.computing_graph import Port
from phot_engine.computing_graph.vertex import Vertex
from phot_engine.globals.engine_exception import VerificationError


def _verify_instantiation_parameter(vertex_dict, edge_list):
    if isinstance(vertex_dict, dict) and isinstance(edge_list, list):
        return True
    else:
        return False


class PhotLabGraph:
    __MAX_NONE_VERTEX_INPUT_EDGE = 10  # 空顶点的最大输入边条数,用于承载计算图中最后一列顶点的输出
    __MAX_COMPUTATIONAL_GRAPH_ROW_NUMBER = 2048
    __vertex_dict = dict()
    __computational_graph = None

    def __init__(self, vertex_dict, edge_list):
        self.__edge_list = []
        if _verify_instantiation_parameter(vertex_dict, edge_list):
            self.__vertex_dict = vertex_dict
            self.__edge_list = edge_list
            self.__replenish_vertex_data()
            # 需要根据生成的拓扑关系生成最终空顶点的拓扑关系
            self.__gen_final_topological_relation()
            self.__gen_computational_graph()
            # print(self.__vertex_dict)
        else:
            raise VerificationError("Can't pass function entry check")

    def get_computational_graph(self):
        return self.__computational_graph

    def get_edge_list(self):
        return self.__edge_list

    def set_edge_list(self, edge_list):
        self.__edge_list = edge_list
        self.__replenish_vertex_data()
        # print("Set graph edge list,list id:{},result:{}".format(id(self.__edge_list), self.__edge_list[0].signal))

    def __calc_indegree(self, vertex_id):
        cnt = 0
        for edge in self.__edge_list:
            if vertex_id == edge.destination_port.vertex_id:
                cnt += 1
        return cnt

    def __calc_outdegree(self, vertex_id):
        cnt = 0
        for edge in self.__edge_list:
            if vertex_id == edge.source_port.vertex_id:
                cnt += 1
        return cnt

    def __fetch_out_edge_iterator(self, vertex_id):
        """
            Get one vertex out edge list
        :param vertex_id:
        :return:
        """
        result_edge_list = []
        for edge in self.__edge_list:
            if vertex_id == edge.source_port.vertex_id:
                result_edge_list.append(edge)
        return result_edge_list

    def __fetch_in_edge_iterator(self, vertex_id):
        """
            Get one vertex in edge list
        :param vertex_id:
        :return:
        """
        result_edge_list = []
        for edge in self.__edge_list:
            if vertex_id == edge.destination_port.vertex_id:
                result_edge_list.append(edge)
        return result_edge_list

    def __replenish_vertex_data(self):
        """
            replenish vertex data like indegree,outdegree,out edge iterator from edge_list
        :return:
        """
        for vertex_id in self.__vertex_dict:
            one_vertex = self.__vertex_dict[vertex_id]
            if isinstance(one_vertex, Vertex):
                indegree = self.__calc_indegree(vertex_id)
                outdegree = self.__calc_outdegree(vertex_id)
                in_edge_iterator = self.__fetch_in_edge_iterator(vertex_id)
                out_edge_iterator = self.__fetch_out_edge_iterator(vertex_id)
                one_vertex.set_indegree(indegree=indegree)
                one_vertex.set_outdegree(outdegree=outdegree)
                one_vertex.set_in_edge_iterator(in_edge_iterator)
                one_vertex.set_out_edge_iterator(out_edge_iterator)
            else:
                pass

    def __fetch_max_degree_number(self) -> int:
        """
            fetch max indegree or outdegree in the vertex dict
        :return:
        """
        max_degree = -1
        for vertex_id in self.__vertex_dict:
            one_vertex = self.__vertex_dict[vertex_id]
            temp_degree = max(one_vertex.get_indegree(), one_vertex.get_outdegree())
            if temp_degree > max_degree:
                max_degree = temp_degree
        return max_degree

    def __is_vertex_id_in_computational_graph(self, vertex_id) -> bool:
        """
            Find a vertex_id if is in computational graph or not
        :param vertex_id:
        :return:
        """
        if self.__computational_graph is not None:
            row, col = self.__computational_graph.shape
            for i in range(0, row):
                for j in range(0, col):
                    if vertex_id == self.__computational_graph[i][j]:
                        return True
            return False
        else:
            return False

    def __fetch_0_indegree_vertex_id_list(self) -> list:
        """
            get all vertexes' id whose indegree = 0
        :return:
        """
        vertex_id_list = []
        for vertex_id in self.__vertex_dict:
            one_vertex = self.__vertex_dict[vertex_id]
            if 0 == one_vertex.get_indegree():
                vertex_id_list.append(one_vertex.vertex_id)
        return vertex_id_list

    def __fetch_0_outdegree_vertex_id_list(self) -> list:
        """
            get all vertexes' id whose outdegree = 0
        :return:
        """
        vertex_id_list = []
        for vertex_id in self.__vertex_dict:
            one_vertex = self.__vertex_dict[vertex_id]
            if 0 == one_vertex.get_outdegree():
                vertex_id_list.append(one_vertex.vertex_id)
        return vertex_id_list

    def __replenish_computational_graph(self, vertex_id_ls):
        """
            按列补全计算图
            Complete the calculation graph by column
        :param vertex_id_ls:
        :return:
        """
        if self.__computational_graph is None:
            self.__computational_graph = np.ndarray(shape=(self.__MAX_COMPUTATIONAL_GRAPH_ROW_NUMBER, 1), dtype=object)
            for row_index in range(0, len(vertex_id_ls)):
                self.__computational_graph[row_index][0] = vertex_id_ls[row_index]
        else:

            one_col = np.ndarray(shape=(self.__MAX_COMPUTATIONAL_GRAPH_ROW_NUMBER, 1), dtype=object)
            for row_index in range(0, len(vertex_id_ls)):
                one_col[row_index][0] = vertex_id_ls[row_index]
            self.__computational_graph = np.column_stack((self.__computational_graph, one_col))

    def __gen_none_vertex(self, number) -> list:
        """
            generate none vertex list
        :param number:
        :return:
        """
        none_vertex_list = []
        for i in range(1, number + 1):
            none_vertex = Vertex(vertex_id="NoneVertex_" + str(i),
                                 phot_lib_class_name=None,
                                 phot_lib_func_name=None,
                                 is_analyzer=False,
                                 parallel_computing_method=None,
                                 vertex_param_list=None)
            self.__vertex_dict[none_vertex.get_vertex_id()] = none_vertex
            none_vertex_list.append(none_vertex)
        return none_vertex_list

    def __connect_none_vertex(self, outdegree_0_vertex_id_list, none_vertex_list):
        for vertex_index in range(0, len(outdegree_0_vertex_id_list)):
            for i in range(0, self.__MAX_NONE_VERTEX_INPUT_EDGE):
                source_port = Port(outdegree_0_vertex_id_list[vertex_index], str(i))
                destination_port = Port(none_vertex_list[vertex_index].get_vertex_id(), str(i))
                one_edge = Edge(source_port, destination_port, "")
                self.__edge_list.append(one_edge)

    def __gen_final_topological_relation(self):
        """
            In order to generate the final topological relationship,
            to fetch output signal from the vertex which outdegree is 0
            1. append NULL Vertex number len(outdegree_0_vertex_id_list)
            2. For a vertex in outdegree_0_vertex_id_list appends enough edge
            3. connect vertex in outdegree_0_vertex_id_list and NULL Vertex
            4.Alter in-out degree number of None Vertex and outdegree 0 vertex
        :return:
        """
        outdegree_0_vertex_id_list = self.__fetch_0_outdegree_vertex_id_list()
        none_vertex_list = self.__gen_none_vertex(len(outdegree_0_vertex_id_list))
        self.__connect_none_vertex(outdegree_0_vertex_id_list=outdegree_0_vertex_id_list,
                                   none_vertex_list=none_vertex_list)
        for none_vertex in none_vertex_list:
            in_edge_iterator = self.__fetch_in_edge_iterator(none_vertex.get_vertex_id())
            out_edge_iterator = self.__fetch_out_edge_iterator(none_vertex.get_vertex_id())
            none_vertex.set_indegree(len(in_edge_iterator))
            none_vertex.set_outdegree(0)
            none_vertex.set_in_edge_iterator(in_edge_iterator)
            none_vertex.set_out_edge_iterator(out_edge_iterator)
        for outdegree_0_vertex_id in outdegree_0_vertex_id_list:
            outdegree_0_vertex = self.__vertex_dict[outdegree_0_vertex_id]
            out_edge_iterator = self.__fetch_out_edge_iterator(outdegree_0_vertex.get_vertex_id())
            outdegree_0_vertex.set_outdegree(self.__MAX_NONE_VERTEX_INPUT_EDGE)
            outdegree_0_vertex.set_out_edge_iterator(out_edge_iterator)
        logging.debug("Generate all None vertexes")

    def __gen_computational_graph(self):
        """
            generate computational graph:
                1. fetch max array column size by max indegree or outdegree in the vertex dict
                2. get all vertexes whose indegree = 0 as initial array as array a
                3. generate adjacent output vertex id list as array b
        :return:
        """
        array_a = self.__fetch_0_indegree_vertex_id_list()
        self.__replenish_computational_graph(array_a)
        while len(array_a) > 0:
            array_b = self.__fetch_adjacent_output_vertex_id_list(array_a)
            self.__replenish_computational_graph(array_b)
            array_a = array_b
        logging.debug("Computational graph has generated successfully.")

        # print(self.__computational_graph)

    def __fetch_adjacent_output_vertex_id_list(self, vertex_id_list):
        """
            给定一个顶点id列表,算法会在图顶点字典中自动寻找邻接的输出顶点，返回它的顶点id列表
            Given a list of vertex id,
            the algorithm automatically looks for the adjacent output vertex in the graph vertex dictionary
            and returns a list of its vertex id
        :param vertex_id_list:
        :return:
        """
        # print("Vertex id list",vertex_id_list)
        before_filter_result_vertex_id_set = set()
        result_adjacent_vertex_id_list = []
        # find all adjacent output vertex id
        for vertex_id in vertex_id_list:
            one_vertex = self.__vertex_dict[vertex_id]
            for edge in one_vertex.get_out_edge_iterator():
                before_filter_result_vertex_id_set.add(edge.destination_port.vertex_id)
        # filter adjacent output vertex id by indegree number and indegree edge list
        # print("before filter vertex id: ", before_filter_result_vertex_id_set)
        for adjacent_vertex_id in before_filter_result_vertex_id_set:
            """
                找到图顶点的邻接输出边，进而得到邻接输出顶点的方法是不完全正确的。
                对于找到的邻接输出顶点若是存在多个输入，但是其输入顶点在当前的计算图中不存在
                那么该顶点无法直接参与运算，需要将运算顺序后移
                It is not completely correct to find the critical output edges of graph vertices 
                and then get the adjacent output vertices.
                If there are multiple inputs to the found adjacent output vertices, 
                the input vertices do not exist in the current graph
                Then the vertex can not directly participate in the operation, need to move the operation order
            """
            one_vertex = self.__vertex_dict[adjacent_vertex_id]
            indegree = one_vertex.get_indegree()
            input_edge_iterator = self.__fetch_in_edge_iterator(adjacent_vertex_id)
            cnt = 0
            for edge in input_edge_iterator:
                if self.__is_vertex_id_in_computational_graph(edge.source_port.vertex_id):
                    cnt += 1
            if cnt == indegree:
                result_adjacent_vertex_id_list.append(adjacent_vertex_id)
        # print("can calculate vertex id in next super step: ", result_adjacent_vertex_id_list)
        return result_adjacent_vertex_id_list

    def get_vertex(self, vertex_id) -> Vertex:
        """
            get a vertex by vertex_id
        :param vertex_id:
        :return:
        """
        if vertex_id in self.__vertex_dict.keys():
            return self.__vertex_dict[vertex_id]
        else:
            raise KeyError("vertex_id:{} does not exist in self.__vertex_dict".format(vertex_id))

    def get_all_analyzer_vertex(self) -> list:
        """
            Get all analyzer vertexes from the whole graph
        :return: list
        """
        analyzer_vertex_list = []
        for vertex_id in self.__vertex_dict:
            if self.__vertex_dict[vertex_id].judge_analyzer():
                analyzer_vertex_list.append(self.__vertex_dict[vertex_id])
        return analyzer_vertex_list

    def clear_all_signal(self):
        """
            Clear all graph's signals
        :return:
        """
        for edge in self.__edge_list:
            edge.signal = None
        for vertex_key in self.__vertex_dict.keys():
            self.__vertex_dict[vertex_key].set_in_edge_iterator(edge_list=[])
            self.__vertex_dict[vertex_key].set_out_edge_iterator(edge_list=[])
        logging.debug("Clear all signals in PhotLabGraph.")

    def show_computational_graph(self):
        row, col = self.__computational_graph.shape
        print("**********Computational Graph**********")
        for i in range(0, row):
            for j in range(0, col):
                if self.__computational_graph[i][j] is not None:
                    print(self.__computational_graph[i][j], end=",")
            if self.__computational_graph[i][0] is not None:
                print()
        print("Graph shape: ", self.__computational_graph.shape)
        print("***************************************")
